from PySide6.QtCore import QLineF, QPoint, QPointF, QRectF, Signal, Qt, Slot
from PySide6.QtGui import (
    QImage,
    QColor,
    QCursor,
    QFocusEvent,
    QFont,
    QMouseEvent,
    QPainter,
    QPen,
    QWheelEvent,
    QPixmap,
)
from PySide6.QtWidgets import (
    QGraphicsItem,
    QGraphicsSimpleTextItem,
    QGraphicsView,
    QGraphicsScene,
    QMessageBox,
    QWidget,
    QStyleOptionGraphicsItem,
)
from PIL import Image
from PIL.ImageQt import ImageQt
import typing
from typing import Dict, Optional
from math import ceil


__all__ = ['PointMarkItem', 'ImageArea']


class PointMarkItem(QGraphicsItem):
    '''
            vertex1
              ▏
              ▏ (0, 0)
             ╱╲
            ╱  ╲
    vertex3     vertex2

    The actual size has a offset caused by line width.
    '''

    # point_pointed = Signal(int)  # type: ignore
    # point_pointless = Signal(int)  # type: ignore

    def __init__(self, p: QPointF, id: int, a: float = 30.0, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.id = id
        self.vertex1 = QPointF(0, -a)
        self.vertex2 = QPointF(0.8 * a, 0.5 * a)
        self.vertex3 = QPointF(-0.8 * a, 0.5 * a)
        self.top_left = QPointF(self.vertex3.x(), self.vertex1.y())
        self.setPos(p)
        self.pen: Optional[QPen] = None
        self.pen_focused: Optional[QPen] = None
        self.setFlag(QGraphicsItem.ItemIsFocusable)

    # override
    @staticmethod
    def type():
        return QGraphicsItem.UserType + 1  # type: ignore

    # override
    def boundingRect(self) -> QRectF:
        if self.pen is None:
            return QRectF(self.top_left, self.vertex2)
        else:
            thickness = self.pen.widthF()
            border_offset = QPointF(thickness / 2, thickness / 2)
            return QRectF(self.top_left - border_offset, self.vertex2 + border_offset)

    def set_pen(self, pen: QPen):
        pen.setCapStyle(Qt.RoundCap)
        self.pen = pen

    def set_pen_focused(self, pen: QPen):
        pen.setCapStyle(Qt.RoundCap)
        self.pen_focused = pen

    # override
    def paint(
        self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: Optional[QWidget]
    ) -> None:
        if self.pen is not None:
            painter.setPen(self.pen)
        center = QPointF(0, 0)
        l1 = QLineF(center, self.vertex1)
        l2 = QLineF(center, self.vertex2)
        l3 = QLineF(center, self.vertex3)
        if self.hasFocus():
            b1 = QLineF(self.vertex1, self.vertex2)
            b2 = QLineF(self.vertex2, self.vertex3)
            b3 = QLineF(self.vertex3, self.vertex1)
            if self.pen_focused is not None:
                painter.setPen(self.pen_focused)
            painter.drawLines((b1, b2, b3))
        painter.drawLines((l1, l2, l3))

    def focusInEvent(self, event: QFocusEvent) -> None:
        self.update()
        # self.point_pointed.emit(self.id)   # type: ignore
        return super().focusInEvent(event)

    def focusOutEvent(self, event: QFocusEvent) -> None:
        self.update()
        # self.point_pointless.emit(self.id)    # type: ignore
        return super().focusOutEvent(event)


class ImageArea(QGraphicsView):
    # current cursor position (QPointF)
    mouse_pos_changed = Signal(QPointF)

    # picture (width, height)
    pix_changed = Signal(int, int)

    # A point with id (int) is placed at (QPointF) with the color at the position as (QColor)
    # point_pointed signal will be emitted immediately after this signal.
    point_placed = Signal(int, QPointF, QColor)

    # A point is focused with the id as (int)
    # this signal is immediately emitted after point_placed signal.
    point_pointed = Signal(int)

    # None of points is focused.
    point_pointless = Signal()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setCursor(QCursor(Qt.CrossCursor))
        self.setRenderHint(QPainter.Antialiasing)
        self.scene: QGraphicsScene = QGraphicsScene()
        self.setScene(self.scene)
        self.draw_center_cross()
        self.pix = self.scene.addPixmap(QPixmap(':/pics/idle.png'))
        self.previous_mouse_point: Optional[QPointF] = None
        self.right_pressed = False
        self.left_pressed = False
        self.ready_to_be_pointed = False
        self.img_loaded = False
        self.img: Optional[QImage] = None
        self.point_id_counter: int = 0
        self.point_list: Dict[int, PointMarkItem] = {}
        self.point_group = self.scene.createItemGroup(())
        self.scene.focusItemChanged.connect(self.on_focus_changed)  # type: ignore

    def draw_center_cross(self, p: QPointF = QPointF(0, 0), color=Qt.magenta):
        x = p.x()
        y = p.y()
        font = QFont()
        font.setPointSize(16)
        line_pen = QPen(color, 10)
        line_x = QLineF(-100.0 + x, x, 500 + x, x)
        line_y = QLineF(y, -100.0 + y, y, 500 + y)
        self.scene.addLine(line_x, line_pen)
        self.scene.addLine(line_y, line_pen)
        text_x = QGraphicsSimpleTextItem('X')
        text_x.setFont(font)
        text_x.setPos(-30.0, -15.0)
        self.scene.addItem(text_x)
        text_y = QGraphicsSimpleTextItem('Y')
        text_y.setFont(font)
        text_y.setPos(-15.0, -20.0)
        text_y.setRotation(270)
        self.scene.addItem(text_y)

    def draw_point(
        self, id: int, p: QPointF, *, color=Qt.magenta, color_focused=Qt.cyan, width=2
    ) -> PointMarkItem:
        mark = PointMarkItem(p, id)
        self.point_list[self.point_id_counter] = mark
        mark.set_pen(QPen(color, width))
        mark.set_pen_focused(QPen(color_focused, width))
        self.point_group.addToGroup(mark)
        return mark

    def place_point(self, p: QPointF):
        if not self.img_loaded:
            return
        assert self.img is not None
        s_p = self.mapToScene(p.toPoint())
        i_p = self.pix.mapFromScene(s_p)
        if not self.pix.boundingRect().contains(i_p):
            return
        mark = self.draw_point(self.point_id_counter, s_p)
        color = self.img.pixelColor(i_p.toPoint())
        self.point_placed.emit(self.point_id_counter, i_p, color)  # type: ignore
        self.point_id_counter += 1
        mark.setFocus(Qt.MouseFocusReason)

    def remove_all_point(self):
        self.scene.removeItem(self.point_group)
        self.point_list.clear()
        self.point_group = self.scene.createItemGroup(())
        self.point_id_counter = 0

    def remove_point(self, id: int):
        if id not in self.point_list:
            raise ValueError(f'PointMarkItem with id {id} is not existed.')
        point = self.point_list.pop(id)
        self.scene.removeItem(point)

    # Set the specific PointMarkItem focused.
    # A point_pointed signal will be triggered.
    def point_to(self, id: int):
        self.point_list[id].setFocus()

    def get_color_by_point(self, p: PointMarkItem) -> QColor:
        pos = p.pos()
        pix_pos = self.pix.mapFromScene(pos)
        assert self.img is not None
        return self.img.pixelColor(pix_pos.toPoint())

    def get_color_by_id(self, id: int) -> QColor:
        point = self.point_list[id]
        return self.get_color_by_point(point)

    def get_pos_by_id(self, id: int) -> QPointF:
        return self.point_list[id].pos()

    def set_pix(self, img: Image.Image) -> None:
        img = img.convert('RGB')
        q_img = ImageQt(img)
        self.img = q_img
        self.pix.setPixmap(QPixmap.fromImage(q_img))
        self.img_loaded = True
        self.pix_changed.emit(img.width, img.height)  # type: ignore

    @Slot(QGraphicsItem, QGraphicsItem, Qt.FocusReason)  # type: ignore
    def on_focus_changed(self, new_: QGraphicsItem, old_: QGraphicsItem, reason: Qt.FocusReason):
        # signal wont be triggered if focus change is not caused by direct mouse click to prevent signal loop.
        if reason is not Qt.MouseFocusReason:
            return
        if new_ is None:
            self.point_pointless.emit()  # type: ignore
            return
        assert new_.type() == PointMarkItem.type()
        new_point = typing.cast(PointMarkItem, new_)
        self.point_pointed.emit(new_point.id)  # type: ignore

    def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
        if event.button() == Qt.LeftButton:
            p = event.position()
            self.place_point(p)
            event.accept()
        else:
            super().mouseDoubleClickEvent(event)

    # override
    def mousePressEvent(self, event: QMouseEvent) -> None:
        if event.button() == Qt.RightButton:
            event.accept()
            self.right_pressed = True
            self.previous_mouse_point = event.position()
            self.setCursor(QCursor(Qt.OpenHandCursor))
        # elif event.button() == Qt.LeftButton:
        #     event.accept()
        #     self.left_pressed = True
        else:
            return super().mousePressEvent(event)

    # override
    def mouseReleaseEvent(self, event: QMouseEvent) -> None:
        if event.button() == Qt.RightButton:
            event.accept()
            self.right_pressed = False
            self.setCursor(QCursor(Qt.CrossCursor))
        # elif event.button() == Qt.LeftButton and self.left_pressed:
        #     event.accept()
        #     self.mouse_click_event(event.position())
        else:
            return super().mouseReleaseEvent(event)

    def scroll_by_points(self, p0: QPointF, p1: QPointF):
        delta_p = (p1 - p0).toPoint()
        delta_x = delta_p.x()
        delta_y = delta_p.y()
        vy: int = self.verticalScrollBar().value()
        self.verticalScrollBar().setValue(vy - delta_y)
        vx = self.horizontalScrollBar().value()
        self.horizontalScrollBar().setValue(vx - delta_x)

    # override
    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        view_pos = event.position()
        pos = self.mapToScene(view_pos.toPoint())
        pos = self.pix.mapFromScene(pos)
        self.mouse_pos_changed.emit(pos)  # type: ignore
        if self.left_pressed:
            self.left_pressed = False
        if self.right_pressed:
            assert self.previous_mouse_point is not None
            self.scroll_by_points(self.previous_mouse_point, view_pos)
            self.previous_mouse_point = view_pos
            event.accept()
        else:
            super().mouseMoveEvent(event)

    # override
    def wheelEvent(self, event: QWheelEvent) -> None:
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        delta = event.angleDelta().y()
        factor = 1.15
        if delta > 0:
            # Zoom in
            self.scale(factor, factor)
        else:
            # Zoom out
            self.scale(1.0 / factor, 1.0 / factor)
